<?php
/**
 * @Author: Marte
 * @Date:   2020-06-15 19:03:02
 * @Last Modified by:   Marte
 * @Last Modified time: 2020-06-30 18:16:43
 */


class Daemon
{
    /**
     * 进程启动时间
     * @var int
     */
    protected $start_time = 0;

    /**
     * 保存主进程id（master pid）的文件
     * @var string
     */
    public $pid_file = "/tmp/daemon.pid";

    /**
     * 解析命令，根据命令做出不同的操作
     *  - start  : 启动进程
     *  - reload : 拼花关闭所有进程重启
     *  - stop   : 平滑停止所有进程，slaver进程循环中，会处理完正在处理的逻辑然后退出
     *  - status : 展示master、slaver进程的信息
     */
    public function parse($argv)
    {
        $commands = [
            "start",
            "stop",
            "reload",
            "status"
        ];
        $command = @trim($argv[1]);

        if (!in_array($command, $commands)) {
            exit('使用方法：php daemon.php [start | stop | reload | status]');
        }

        switch ($command) {
            case "stop":
            {
                if(file_exists($this->pid_file)){
                    $pid = file_get_contents($this->pid_file);
                    posix_kill($pid, SIGKILL);
                    unlink($this->pid_file);
                }

                echo '停止成功';
            }
                break;

            case "status":
            {
                if(file_exists($this->pid_file)){
                    $pid = file_get_contents($this->pid_file);
                    posix_kill($pid, SIGUSR1);

                    $key = ftok(__FILE__,'a');
                    $queue = msg_get_queue($key, 0666);
                    //接收消息，如果接收不到会阻塞
                    msg_receive($queue, 1, $msg_type, 1024, $message);
                    echo $message;
                    //移除消息
                    msg_remove_queue($queue);
                }
            }
                break;

            case "reload":
            {
                if(file_exists($this->pid_file)){
                    $pid = file_get_contents($this->pid_file);
                    posix_kill($pid, SIGKILL);
                }
            }

            case "start" || "reload":
            {
                $this->fork();

                $this->savePid();

                $this->resetStd();

                $this->installSignal();

                $this->monitor();
            }
                break;

            default:
                # code...
                break;
        }
    }

    protected function fork()
    {
        //创建一个子进程,父进程内返回子进程ID,子进程内返回0
        $pid = pcntl_fork();
        if($pid < 0){
            exit('fork error.');
        }else if($pid > 0){
            exit();
        }

        //创建会话,使子进程成为组长进程,需要把父进程杀掉
        if(posix_setsid() < 0){
            exit('setsid error.');
        }

        //再次创建子进程，为了防止在SVR4的系统下重新打开控制终端
        $pid = pcntl_fork();
        if($pid  < 0){
            exit('fork error');
        }else if($pid > 0){
            // 主进程退出
            exit;
        }

        // 修改工作目录，让进程脱离原来的目录
        if(!chdir("/")){
            exit('chdir error.');
        }

        //相当于chmod 777
        umask(0);


        //设置进程名称
        cli_set_process_title('test');
    }

    /**
     * 保存进程号文件到制定文件中
     * @return [type] [description]
     */
    protected function savePid()
    {
        $pid = posix_getpid();
        if (false === @file_put_contents($this->pid_file, $pid)) {
            exit("主进程id（master pid）写入文件[".$this->pid_file."]失败.");
        }

        $this->start_time = time();
    }

    protected function resetStd()
    {
        //文件描述符必须声明为全局变量，否则将在函数执行完毕后被释放
        global $stdin, $stdout, $stderr;

        //关闭打开的文件描述符
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);

        //关闭了文件描述符，打开的前三个文件描述符将成为新的描述符
        $stdin = fopen('/dev/null', 'r');
        $stdout = fopen('/dev/null', 'a');
        $stderr = fopen('/dev/null', 'a');
    }

    /**
     * 注册信号
     */
    protected function installSignal()
    {
        // 平滑退出
        pcntl_signal(SIGINT, [__CLASS__, "handleSignal"], false);
        // 暴力退出
        pcntl_signal(SIGTERM, [__CLASS__, "handleSignal"], false);
        // 查看进程状态
        pcntl_signal(SIGUSR1, [__CLASS__, "handleSignal"], false);
    }

    /**
     * 处理进程的信号
     * @param  int 信号触发回调的时候传递过来的信号标示值
     */
    protected function handleSignal($signal)
    {
        // kill -l
        switch ($signal) {
            // kill -2
            case SIGINT:
            {
                $pid = file_get_contents($this->pid_file);
                // 向进程发送信号
                posix_kill($pid, SIGINT);
            }
                break;
            // kill -15
            case SIGTERM:
            {
                $pid = file_get_contents($this->pid_file);
                posix_kill($pid, SIGKILL);
            }
                break;
            case SIGUSR1:
            {
                // 获取进程状态
                $pid = posix_getpid();
                $memory = round(memory_get_usage(true) / (1024 * 1024), 2) . "M";
                $time = time();
                $class_name = get_class($this);
                $start_time = date("Y-m-d H:i:s", $this->start_time);
                $run_day = floor(($time - $this->start_time) / (24 * 60 * 60));
                $run_hour = floor((($time - $this->start_time) % (24 * 60 * 60)) / (60 * 60));
                $run_min = floor(((($time - $this->start_time) % (24 * 60 * 60)) % (60 * 60)) / 60);
                $status = "Daemon [{$class_name}] 信息: \n"
                    ."-------------------------------- master进程状态 --------------------------------\n"
                    .str_pad("pid", 10)
                    .str_pad("占用内存", 19)
                    .str_pad("处理次数", 19)
                    .str_pad("开始时间", 29)
                    .str_pad("运行时间", 34)
                    ."\n"
                    .str_pad($pid, 10)
                    .str_pad($memory, 15)
                    .str_pad("--", 15)
                    .str_pad($start_time, 25)
                    .str_pad("{$run_day} 天 {$run_hour} 时 {$run_min} 分", 30)
                    ."\n";

                $key = ftok(__FILE__,'a');
                //获取消息队列
                $queue = msg_get_queue($key, 0666);
                //发送消息, 查看命令ipcs -qt
                msg_send($queue, 1, $status);
            }
                break;
            default:
                # code...
                break;
        }
    }

    protected function monitor()
    {
        while (true) {

            sleep(1);
            //调用该方法，信号处理函数才会被执行
            pcntl_signal_dispatch();
        }
    }
}


$cli = new Daemon();
$cli->parse($argv);